
/*

DEPENDENCIES:
-------------
Gimme.Helper
ext.for_each
-------------


IN THIS FILE:
-------------
ext.add_event
ext.remove_event
-------------

*/

(function()
{
	var help = Gimme.Helper;
	var internals = Gimme.Internals;

	// last listening ancestor (tracks the event chain so we know when to properly reset the eventPhase variable)
	var lla;

	// eventPhase keeps track of which phase of event propagation we're in: 1 = capturing, 3 = bubbling, 2 = at target
	// (note that this is possible because of the single threaded nature of Javascript events)
	var eventPhase;

	var handlerHash = {};
	var handlerCountHash = {};
	var capturingEventHash =
	{
		load: false,
		unload: false,
		focus: false,
		blur: false,
		beforeunload: false,
		stop: false,
		beforeprint: false,
		afterprint: false,
		propertychange: false,
		filterchange: false,
		readystatechange: false,
		losecapture: false
	};
	
	Gimme.ext.add_event = function()
	{
		return document.addEventListener ?
			w3c :								// use w3c style event handling
			document.attachEvent ?
			ie :								// use ie style event handling
			function()
			{									// no modern event support :-(
			};

		function w3c(eventName, handler, useCapture, pseudoCall)
		{
			if (!pseudoCall && Gimme.Events && Gimme.Events.Pseudo && Gimme.Events.Pseudo[eventName])
			{
				Gimme.Events.Pseudo[eventName].add(this, handler, useCapture);
			}
			else
			{
				this.for_each(function(elem)			
				{
					internals.addEventListener.call(elem, eventName, handler, useCapture);
				});
			}
			
			return this;
		}
		
		function ie(eventName, handler, useCapture, pseudoCall)
		{
			function normalizeEvent(elem, e)
			{
				// set the eventPhase
				// 1 = capturing (the default start value, but this can change during the event's lifecycle)
				// 2 = at target
				// 3 = bubbling
				eventPhase = eventPhase || 1;
				e.eventPhase = eventPhase;
				
				// map .target to .srcElement
				e.target = e.srcElement;
				
				// map .currentTarget to _elem (so the element dispatching the events can be accessed)
				e.currentTarget = elem;

				// augment the event so that mouse/cursor position can be retrieved via .pageX/Y
				var mousePos = Gimme.Screen.get_mouse_pos(e);			
				e.pageX = mousePos.x;
				e.pageY = mousePos.y;
				
				switch (e.type)
				{
					case 'keyup':
					case 'keydown':
					case 'keypress':
						// IE no likely when you try to monkey with key* properties of a file input
						if (elem.type !== 'file')
						{
							// try to add some sanity to the which/keyCode mess (see http://unixpapa.com/js/key.html)
							e.keyCode === 186 ? e.keyCode = 59 :				// map IE's keyCode for ";:" to Mozilla's
							e.keyCode === 187 ? e.keyCode = 61 :				// map IE's keyCode for "=+" to Mozilla's
							e.keyCode === 189 ? e.keyCode = 109 : null;			// map IE's keyCode for "-_" to Mozilla's
						}
						e.which = e.keyCode;									// e.which is either e.keyCode or e.charCode, whichever is "set" (applicable only to keydown)
						e.charCode = e.keyCode;									// in IE: keypress/keyCode is charCode, keydown/charCode is not useful and should not be used
						break;
						
					case 'mousedown':
					case 'mouseup':
					case 'click':
					case 'dblclick':
					case 'contextmenu':
						// nifty IE e.button to FF e.which mapping
						e.which =  [ 1, 1, 3, e.button, 2 ][e.button];		// 3 should never happen in IE (it uses bit flags), but stuff in e.button just in case
						break;
						
					case 'mouseover':
						// map .relatedTarget to either .toElement
						e.relatedTarget = e.fromElement;
						break;
						
					case 'mouseout':
						// or .fromElement
						e.relatedTarget = e.toElement;
						break;
				}
				
				// add in support for .preventDefault() and .stopPropagation()
				e.preventDefault = function()
				{
					e.returnValue = false;
				};
				e.stopPropagation = function()
				{
					e.cancelBubble = true;
				};
				
				return e;
			};
		
			function storeCaptureData(eventName, elemGuid, handler)
			{
				var capturingEvent = capturingEventHash[eventName];
				if (capturingEvent === false)
				{
					// false means that event propagation is not supported on this event type,
					// we'll return false to indicate that
					return false;
				}

				if (!capturingEvent)
				{
					capturingEvent = capturingEventHash[eventName] = {};
				}

				var handlers = capturingEvent[elemGuid];
				if (!handlers)
				{
					handlers = capturingEvent[elemGuid] = [];
				}

				handlers.push(handler);

				// returning true means event capturing is supported for this event
				return true;
			}

			// increments the handler count for a given eventType / element combination
			function incHandlerCount(eventName, elemGuid)
			{
				var evt = handlerCountHash[eventName];
				if (!evt)
				{
					evt = handlerCountHash[eventName] = {};
				}

				var count = evt[elemGuid];
				if (!count)
				{
					count = evt[elemGuid] =
					{
						// total number of handlers registered to the element for the given event type
						handlerCount: 0,

						// number of handlers left to execute during an event cycle (see proxy function in the ie add_event logic)
						remainingHandlers: 0
					};
				}

				count.handlerCount++;
				count.remainingHandlers = count.handlerCount;
			}
			
			function handleCapturingPhase(elem, e)
			{
				// determine the stack of ancestors that need to be checked for event capturing
				var stack = [];
				var parent = elem;
				while (parent)
				{
					stack.push(parent);
					parent = parent.parentNode;
				}
				
				// execute all handler for any element that is listening for "captured" events,
				// but only if propagation /hasn't/ been stopped (e.cancelBubble = true)
				var current;
				while (current = stack.pop())
				{
					var currentGuid = help.get_guid(current);
					if (capturingEventHash[eventName][currentGuid])
					{
						if (!lla)
						{
							// find the ancestor highest up in the document hierarchy that is
							// listening for an event of the given type (this, along with an
							// examination of e.cancelBubble, tells us when event propagation
							// has come to an end
							lla = current;
						}

						var i, len = capturingEventHash[eventName][currentGuid].length;
						for (i = 0; i < len; i++)
						{
							if (!e.cancelBubble)
							{
								// if we're at the target, set eventPhase accordingly
								if (current === e.target)
								{
									e.eventPhase = eventPhase = 2;
								}
								var eventHash = capturingEventHash[eventName];								
								eventHash[currentGuid] && eventHash[currentGuid][i] && eventHash[currentGuid][i].call(current, e);
							}
						}
					}
				}

				return lla;
			}
			
			if (!pseudoCall && Gimme.Events && Gimme.Events.Pseudo && Gimme.Events.Pseudo[eventName])
			{
				Gimme.Events.Pseudo[eventName].add(this, handler, useCapture);
			}
			else
			{
				this.for_each(function(elem)
				{				
					// create a key to identify this element/event/function combination
					var elemGuid = help.get_guid(elem);
					var handlerGuid = help.get_guid(handler) + (useCapture || false).toString();
					var key = '{' + elemGuid + ' / ' + eventName + ' / ' + handlerGuid + '}';			
					
					if (useCapture)
					{
						// if event capturing isn't supported on this event type, we'll find out here
						useCapture = storeCaptureData(eventName, elemGuid, handler)
					}
					
					var proxy = handlerHash[key];
					if (!proxy)
					{
						proxy = function(e)
						{
							// get the count object for this event/element combination, which
							// includes the number of registered handlers as well as the number
							// remaining that still need to execute
							var count = handlerCountHash[eventName][elemGuid];
							count.remainingHandlers--;

							e = normalizeEvent(elem, e);
							
							if (capturingEventHash[eventName] && eventPhase === 1)
							{
								lla = handleCapturingPhase(elem, e);
							}
							
							eventPhase = elem === e.target ? 2 : 3;  
							e.eventPhase = eventPhase;
							!useCapture && !e.cancelBubble && handler.call(elem, e);

							// if there is no known lla (last listening ancestor),
							// or if propagation has been explicitly stopped (e.cancelBubble),
							// or if the elem that is listening IS the lla AND all of its handlers have executed),
							// then we've come to the end of an event propagation cycle
							if (!lla || e.cancelBubble || (elem == lla && count.remainingHandlers === 0))
							{
								// a few variables need to be reset for the next event cycle that will execute
								eventPhase = 1;
								lla = undefined;
								count.remainingHandlers = count.handlerCount;
							}
							
							// cleanup
							e.eventPhase =
							e.target =
							e.currentTarget =
							e.relatedTarget =
							e.which =
							e.preventDefault =
							e.stopPropagation =
							e.pageX =
							e.pageY = undefined;
						};
						
						handlerHash[key] = proxy;						
						internals.attachEvent.call(elem, 'on' + eventName, proxy);
						
						// record that another handler has been wired up for this event/element combination 
						incHandlerCount(eventName, elemGuid);
					}
					
					key = null;
					proxy = null;
				});
			}
			
			return this;
		}
	}();
	//[AltSyntax]Gimme.ext.addEvent = Gimme.ext.add_event;
	

	Gimme.ext.remove_event = function()
	{
		return document.addEventListener ?
			w3c :								// use w3c style event handling
			document.attachEvent ?
			ie :								// use ie style event handling
			function()
			{									// no modern event support :-(
			};
		
		function w3c(eventName, handler, useCapture, pseudoCall)
		{
			if (!pseudoCall && Gimme.Events && Gimme.Events.Pseudo && Gimme.Events.Pseudo[eventName])
			{
				Gimme.Events.Pseudo[eventName].remove(this, handler, useCapture);
			}
			else
			{
				this.for_each(function(elem)			
				{
					internals.removeEventListener.call(elem, eventName, handler, useCapture);
				});
			}		
			
			return this;
		}
		
		function ie(eventName, handler, useCapture, pseudoCall)
		{
			function removeCaptureData(eventName, elemGuid, handler)
			{
				var capturingEvent = capturingEventHash[eventName];
				if (capturingEvent)
				{
					var handlers = capturingEvent[elemGuid];
					if (handlers)
					{
						var i = Gimme(handlers).index_of(handler);
						if (i !== -1)
						{
							handlers.splice(i, 1);
						}
						if (handlers.length === 0)
						{
							delete capturingEvent[elemGuid];
						}
					}
				}
			}

			
			// decrements handler count for a given eventType / element combination			
			function decHandlerCount(eventName, elemGuid)
			{
				var count = handlerCountHash[eventName] && handlerCountHash[eventName][elemGuid];
				if (count)
				{
					if (count.handlerCount > 0)
					{
						count.handlerCount--;
					}
					
					count.remainingHandlers = count.handlerCount;
				}
			}

			if (!pseudoCall && Gimme.Events && Gimme.Events.Pseudo && Gimme.Events.Pseudo[eventName])
			{
				Gimme.Events.Pseudo[eventName].remove(this, handler, useCapture);
			}
			else
			{			
				this.for_each(function(elem)
				{
					// grab the event handler function from the hash based on its key
					// create a key to identify this element/event/function combination
					var elemGuid = help.get_guid(elem);
					var handlerGuid = help.get_guid(handler) + (useCapture || false).toString();
					var key = '{' + elemGuid + ' / ' + eventName + ' / ' + handlerGuid + '}';
					
					if (useCapture)
					{
						removeCaptureData(eventName, elemGuid, handler)
					}
					
					var proxy = handlerHash[key]
					if (proxy)
					{
						// unhook the event and delete the element from the event hash						
						internals.detachEvent.call(elem, 'on' + eventName, proxy);
						delete handlerHash[key];
						decHandlerCount(eventName, elemGuid);
					}

					// prevent IE memory leaks
					key = null;
					proxy = null;
				});
			}
				
			return this;			
		}
	
	}();
	//[AltSyntax]Gimme.ext.removeEvent = Gimme.ext.remove_event;
})();
